一、静态layout搭建

1.新建layout文件夹

2.修改路由跳转页面地址

3.在scss全局变量中新增一些变量

4.在scss的index.scss中设置滚动条相关设置

5.编写layout静态页面布局

二、logo组件封装

1.新建文件

2.设置图片、文字的常量

3.设置scss的常量值

4.编写logo组件的内容

5.在父组件layout中引入logo组件

6.效果展示

三、左侧菜单的静态搭建

四、递归组件生成动态菜单

1.新建菜单组件

2.修改路由

3.修改用户小仓库类型、用户小仓库添加路由

4. layout组件将静态菜单改造成动态菜单

5.子菜单组件 根据条件显示菜单，并通过递归实现动态菜单

6.测试结果

 五、左侧菜单图标配置

1.在项目中引入element-plus的标签库

2. 修改路由配置

3.菜单组件引入标签，同时点击按钮根据地址进行跳转

4.测试结果

六、项目全部路由配置

1.创建文件

2.配置路由

3. 在layout主页面中引入右侧内容展示

4.在菜单组件中引入路由器

5.接受路由并展示路由内容

6.代码解析 

7.测试结果

七、感谢支持

一、静态layout搭建
 我们前面已经完成登录现在来到了首页。首页中我们有一个左边菜单栏，上面有一个导航栏，然后有一个正文部分。

1.新建layout文件夹
在src下面新建layout文件夹，新建index.vue文件



2.修改路由跳转页面地址
我们将首页的地址变成我们新建的index.vue



3.在scss全局变量中新增一些变量
对于layout中用到的一些scss数值，我们将他们抽取到全局变量中：

// 给项目提供scss的全局变量
$color:blue;

// 定义项目主题颜色

// 左侧菜单的宽度
$base-menu-width:260px;
// 左侧菜单的背景颜色
$base-menu-background:#001529;

// 顶部导航的高度
$base-tabbar-height:50px;


4.在scss的index.scss中设置滚动条相关设置
// 引入清除默认样式
@import './reset.scss';

/* 滚动条外观设置 */
// 滚动条宽度
::-webkit-scrollbar{
    width: 10px;
}
// 滚动条背景色
::-webkit-scrollbar-track{
    background: $base-menu-background;
}
// 滚动的滚轮
::-webkit-scrollbar-thumb{
    width: 10px;
    background-color: cyan;
    // 滚轮变成圆角
    border-radius: 10px;
}


5.编写layout静态页面布局
<template>
    <div class="layout_container">
        <!-- 左侧菜单 -->
        <div class="layout_slider">123</div>
        <!-- 顶部导航 -->
        <div class="layout_tabbar">456</div>
        <!-- 内容展示区域 -->
        <div class="layout_main">
            <p style="height:10000px;background-color:red;">我是一个段落</p>
        </div>
    </div>
</template>

<script setup lang="ts">

</script>

<style scoped lang="scss">
.layout_container {
    width: 100%;
    height: 100vh;
    background: red;
    .layout_slider {
        width: $base-menu-width;
        height: 100vh;
        background: $base-menu-background;
    }
    .layout_tabbar{
        position:fixed;
        /* 顶部导航的宽度 = 总宽度 - 左侧菜单的宽度 */
        width:calc(100% - $base-menu-width);
        height:$base-tabbar-height;
        top:0px;
        left:$base-menu-width;
        background:cyan;

    }
    .layout_main{
        position:absolute;
        width:calc(100% - $base-menu-width);
        height:calc(100vh - $base-tabbar-height);
        background-color:yellow;
        left: $base-menu-width;
        top:$base-tabbar-height;
        /* 内容与边框有些间隔 */
        padding:20px;
        /* 如果内容高度很高，会超出layout的范围。
        加了这个之后就算再高也还是在layout内，只是有了一个滚动条 */
        overflow:auto;
    }
}
</style>


最后效果如下： 



二、logo组件封装
1.新建文件
我们要先把logo.png文件放进public里面，图片自己随便给就行。

在src/layout文件夹下面新建logo/index.vue，用来写logo组件的内容。

在src文件夹下面新建一个setting.ts，用来存放logo组件中的图片、文件静态文件



2.设置图片、文字的常量
在setting.ts中把常量放进去，有的项目如果没有logo组件的话，也要支持不显示logo组件：

setting.ts：

// 用于项目的logo、标题的配置
export default {
    title:'徐鹏运营平台',//项目标题
    logo:'/public/logo.png',//项目logo
    logoShow:true//logo组件是否显示
}


3.设置scss的常量值
 variable.scss：

// 给项目提供scss的全局变量
$color:blue;

// 定义项目主题颜色

// 左侧菜单的宽度
$base-menu-width:260px;
// 左侧菜单的背景颜色
$base-menu-background:#001529;

// 顶部导航的高度
$base-tabbar-height:50px;

// 左侧菜单logo的高度设置
$base-menu-logo-height:50px;

// 左侧菜单logo右侧文字大小
$base-logo-title-fontSize:20px;


4.编写logo组件的内容
我们在这里通过v-if来判断是否显示logo组件

<template>
    <div class="logo" v-if="setting.logoShow">
        <img :src="setting.logo">
        <p>{{ setting.title }}</p>
    </div>
</template>

<script setup lang="ts">
// 引入设置标题、logo的配置文件
import setting from '@/setting';
</script>

<style scoped lang="scss">
.logo{
    width: 100%;
    height: $base-menu-logo-height;
    color: white;
    /* 图标和文字水平排列 */
    display: flex;
    align-items: center;
    /* 图标离最左侧保持一点距离 */
    padding: 20px;
    img{
        width: 40px;
        height: 40px;
    }
    p{
        font-size: $base-logo-title-fontSize;
        /* 图标与文字中间保持一点距离 */
        margin-left: 10px;
    }
}

</style>


5.在父组件layout中引入logo组件
<template>
    <div class="layout_container">
        <!-- 左侧菜单 -->
        <div class="layout_slider">
            <Logo></Logo>
        </div>
        <!-- 顶部导航 -->
        <div class="layout_tabbar">456</div>
        <!-- 内容展示区域 -->
        <div class="layout_main">
            <p style="height:10000px;background-color:red;">我是一个段落</p>
        </div>
    </div>
</template>

<script setup lang="ts">
// 引入左侧菜单logo子组件
import Logo from "./logo/index.vue";
</script>

<style scoped lang="scss">
.layout_container {
    width: 100%;
    height: 100vh;
    background: red;
    .layout_slider {
        width: $base-menu-width;
        height: 100vh;
        background: $base-menu-background;
    }
    .layout_tabbar{
        position:fixed;
        /* 顶部导航的宽度 = 总宽度 - 左侧菜单的宽度 */
        width:calc(100% - $base-menu-width);
        height:$base-tabbar-height;
        top:0px;
        left:$base-menu-width;
        background:cyan;

    }
    .layout_main{
        position:absolute;
        width:calc(100% - $base-menu-width);
        height:calc(100vh - $base-tabbar-height);
        background-color:yellow;
        left: $base-menu-width;
        top:$base-tabbar-height;
        /* 内容与边框有些间隔 */
        padding:20px;
        /* 如果内容高度很高，会超出layout的范围。
        加了这个之后就算再高也还是在layout内，只是有了一个滚动条 */
        overflow:auto;
    }
}
</style>


6.效果展示


三、左侧菜单的静态搭建
我们要先引入滚动组件：el-scrollbar，不然如果菜单列有1000项，会导致菜单列的数据超出layout，用了滚动组件，数据项再多也时在layout中滚动。

滚动组件内再引入菜单组件，其中菜单组件分单个和可折叠的，可折叠的中通过插槽传入一级的名称。

<template>
    <div class="layout_container">
        <!-- 左侧菜单 -->
        <div class="layout_slider">
            <Logo></Logo>
            <!-- 展示菜单 -->
            <!-- 滚动组件 -->
            <el-scrollbar class="scrollbar">
                <!-- 菜单组件 -->
                <el-menu background-color="#001529" text-color="white">
                    <el-menu-item index="1">首页</el-menu-item>
                    <el-menu-item index="2">数据大屏</el-menu-item>
                    <!-- 折叠菜单 -->
                    <el-sub-menu index="3">
                        <template #title>
                            <span>权限管理</span>
                        </template>
                        <el-menu-item index="3-1">用户管理</el-menu-item>
                        <el-menu-item index="3-2">角色管理</el-menu-item>
                        <el-menu-item index="3-3">菜单管理</el-menu-item>
                    </el-sub-menu>
                </el-menu>
            </el-scrollbar>
        </div>
        <!-- 顶部导航 -->
        <div class="layout_tabbar">456</div>
        <!-- 内容展示区域 -->
        <div class="layout_main">
            <p style="height:10000px;background-color:red;">我是一个段落</p>
        </div>
    </div>
</template>

<script setup lang="ts">
// 引入左侧菜单logo子组件
import Logo from "./logo/index.vue";
</script>

<style scoped lang="scss">
.layout_container {
    width: 100%;
    height: 100vh;
    background: red;

    .layout_slider {
        width: $base-menu-width;
        height: 100vh;
        background: $base-menu-background;
        .scrollbar{
            width: 100%;
            height: calc(100vh - $base-tabbar-height);
        }
    }

    .layout_tabbar {
        position: fixed;
        /* 顶部导航的宽度 = 总宽度 - 左侧菜单的宽度 */
        width: calc(100% - $base-menu-width);
        height: $base-tabbar-height;
        top: 0px;
        left: $base-menu-width;
        background: cyan;

    }

    .layout_main {
        position: absolute;
        width: calc(100% - $base-menu-width);
        height: calc(100vh - $base-tabbar-height);
        background-color: yellow;
        left: $base-menu-width;
        top: $base-tabbar-height;
        /* 内容与边框有些间隔 */
        padding: 20px;
        /* 如果内容高度很高，会超出layout的范围。
        加了这个之后就算再高也还是在layout内，只是有了一个滚动条 */
        overflow: auto;
    }
}</style>




效果展示：



四、递归组件生成动态菜单
我们上面菜单栏是写死的index，这肯定不行，需要写成动态菜单。 

1.新建菜单组件
在src/layout下新建menu/index.vue，这是菜单组件。



2.修改路由
我们以前路由是写死的几个固定页面，这里需要改动一下：

首先把/layout对应的页面变更为@/layout/index.vue，然后再layout下面新建两个子路由，然后再给每个路由地址添加一个meta属性，meta属性里面有title（用于菜单页标题显示）、hidden（用于判断该路由是否在菜单栏显示）

// 对外暴露配置的路由(常量路由)
export const constantRoute = [
  {
    // 登录
    path: '/login',
    component: () => import('@/views/login/index.vue'),
    name: 'login', // 命名路由
    meta: {
      title: '登录', //菜单标题
      hidden:true //路由标题在菜单中是否隐藏
    }
  },
  {
    // 登录成功以后展示数据
    path: '/',
    component: () => import('@/layout/index.vue'),
    name: 'layout',
    meta: {
      title: 'layout',
      hidden:false
    },
    children: [
      {
        path: '/home',
        component: () => import('@/views/home/index.vue'),
        meta: {
          title: '首页',
          hidden:true
        }
      },
      {
        path: '/ceshi',
        component: () => import('@/views/home/index.vue'),
        meta: {
          title: '测试',
          hidden:false
        }
      }

    ]
  },
  {
    // 404
    path: '/404',
    component: () => import('@/views/404/index.vue'),
    name: '404',
    meta: {
      title: '404',
      hidden:true
    }
  },
  {
    // 任意路由，就是都没命中的兜底
    path: '/:pathMatch(.*)*',
    redirect: '/404',
    name: 'Any',
    meta: {
      title: '任意',
      hidden:true
    }
  },
]


3.修改用户小仓库类型、用户小仓库添加路由
我们上面的路由改动完之后我们要把路由放到小仓库里。目前我们只做了用户小仓库，所以放到用户小仓库里面。

以前我们用户小仓库只返回token，现在返回值多增加一个路由对象。因此用户小仓库的返回类型type.ts要先改动一下，添加一个路由的出参类型。

然后再用户小仓库user.ts里引用路由，再直接吧路由作为出参返回出去。

type.ts用户小仓库类型：

import type { RouteRecordRaw } from "vue-router"

// 定义小仓库数据state类型
interface UserState {
  token: string | null,
  /* 路由里每一个路由的类型都是：RouteRecordRaw*/
  menuRoutes:[RouteRecordRaw]
}


user.ts用户小仓库引用路由：

// 用户相关小仓库
import { defineStore } from 'pinia'

// 引入接口
import { reqLogin } from '@/api/user'

// 引入数据类型
import type { loginForm, loginResponseData } from '@/api/user/type'
import type { UserState } from './types/type'

// 引入操作本地存储的工具方法
import { SET_TOKEN, GET_TOKEN } from '@/utils/token'

// 引入路由（常量路由）
import {constantRoute} from '@/router/routes'

let useUserStore = defineStore('User', {
  // 小仓库存储数据的地方
  state(): UserState {
    return {
      token: GET_TOKEN(), //用户唯一标识
      menuRoutes:constantRoute, //仓库存储生成菜单需要数组（路由）
    }
  },
  // 异步处理逻辑的地方
  actions: {
    // 用户登录的方法
    async userLogin(data: loginForm) {
      // 登录请求
      let result: loginResponseData = await reqLogin(data)

      // 如果登录成功，存储token到pinia仓库
      if (result.code == 200) {
        this.token = result.data.token as string

        // 本地持久化token
        SET_TOKEN(result.data.token as string)

        return 'ok'
      } else {
        return Promise.reject(new Error(result.data.message))
      }
    },
  },
  getters: {},
})

export default useUserStore


4. layout组件将静态菜单改造成动态菜单
我们上面layout中的index.vue中的菜单是写死的，我们这边要改成引用动态菜单组件：

先引入菜单组件，再引入用户小仓库，拿到里面的路由对象。

然后将路由对象传给子组件。

<template>
    <div class="layout_container">
        <!-- 左侧菜单 -->
        <div class="layout_slider">
            <Logo></Logo>
            <!-- 展示菜单 -->
            <!-- 滚动组件 -->
            <el-scrollbar class="scrollbar">
                <!-- 菜单组件 -->
                <el-menu background-color="#001529" text-color="white">
                    <!-- 根据路由动态生成菜单 -->
                    <Menu :menuList="userStore.menuRoutes"></Menu>
                </el-menu>
            </el-scrollbar>
        </div>
        <!-- 顶部导航 -->
        <div class="layout_tabbar">456</div>
        <!-- 内容展示区域 -->
        <div class="layout_main">
            <p style="height:10000px;background-color:red;">我是一个段落</p>
        </div>
    </div>
</template>

<script setup lang="ts">
// 引入左侧菜单logo子组件
import Logo from "./logo/index.vue";
// 引入菜单组件
import Menu from './menu/index.vue';

// 引入用户小仓库
import useUserStore from '@/store/modules/user';
let userStore = useUserStore();
</script>

<style scoped lang="scss">
.layout_container {
    width: 100%;
    height: 100vh;
    background: white;

    .layout_slider {
        width: $base-menu-width;
        height: 100vh;
        background: $base-menu-background;
        .scrollbar{
            width: 100%;
            height: calc(100vh - $base-tabbar-height);
            /* 菜单栏中右侧会有白色边框，我们隐藏白色边框 */
            .el-menu{
                border-right: none;
            }
        }
    }

    .layout_tabbar {
        position: fixed;
        /* 顶部导航的宽度 = 总宽度 - 左侧菜单的宽度 */
        width: calc(100% - $base-menu-width);
        height: $base-tabbar-height;
        top: 0px;
        left: $base-menu-width;
        background: cyan;

    }

    .layout_main {
        position: absolute;
        width: calc(100% - $base-menu-width);
        height: calc(100vh - $base-tabbar-height);
        background-color: yellow;
        left: $base-menu-width;
        top: $base-tabbar-height;
        /* 内容与边框有些间隔 */
        padding: 20px;
        /* 如果内容高度很高，会超出layout的范围。
        加了这个之后就算再高也还是在layout内，只是有了一个滚动条 */
        overflow: auto;
    }
}</style>


5.子菜单组件 根据条件显示菜单，并通过递归实现动态菜单
其中，递归需要做一次声明，不然递归不会生效：

<template>
    <div>
        <template v-for="(item, index) in menuList" :key="item.path">
            <!-- 没有子路由 -->
            <template v-if="!item.children">
                <!-- 且没有被隐藏 -->
                <el-menu-item :index="item.path" v-if="!item.meta.hidden">
                    <template #title>
                        <span>图标：&nbsp;</span>
                        <span>{{ item.meta.title }}</span>
                    </template>
                </el-menu-item>
            </template>

            <!-- 只有一个子路由 -->
            <template v-if="item.children && item.children.length == 1">
                <el-menu-item :index="item.children[0].path" v-if="!item.children[0].meta.hidden">
                    <template #title>
                        <span>图标：&nbsp;</span>
                        <span>{{ item.children[0].meta.title }}</span>
                    </template>
                </el-menu-item>
            </template>

            <!-- 有多个子路由 -->
            <el-sub-menu v-if="item.children && item.children.length > 1" :index="item.path">
                <template #title>
                    <span>图标：&nbsp;</span>
                    <span>{{ item.meta.title }}</span>
                </template>
                <Menu :menuList="item.children"></Menu>
            </el-sub-menu>

        </template>
    </div>
</template>

<script setup lang="ts">
// 获取父组件传递过来的全部路由数组
defineProps(['menuList'])
</script>

<!-- 递归调用的写法，递归的时候不允许标签没有定义，所以我们在本文件中默认返回Menu做一个定义 -->
<script lang="ts">
export default {
    name: 'Menu'
}
</script>

<style scoped></style>


6.测试结果


 五、左侧菜单图标配置
element-plus的标签库地址，需要的样式在这边直接复制即可：

Icon 图标 | Element Plus

1.在项目中引入element-plus的标签库
index.ts：

// 引入项目中全部的全局组件
import SvgIcon from './SvgIcon/index.vue'
import Pagination from './Pagination/index.vue'

// 引入element-plus的全部图标
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

// 全局对象
const allGlobalComponent = { SvgIcon, Pagination }

// 对外暴露插件对象
export default {
  //名称一定要是install
  install(app) {
    Object.keys(allGlobalComponent).forEach((key: string) => {
      app.component(key, allGlobalComponent[key])
    });

    // 将element-plus提供的图标都注册为全局图标
    for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
      app.component(key, component)
    }
  },
}